package lol.clann.minecraft.springboot.test;

import com.github.azbh111.utils.java.io.IOUtils;
import lol.clann.minecraft.springboot.api.model.Result;
import lol.clann.minecraft.springboot.remap.ASMUtils;
import net.minecraft.server.v1_9_R2.BlockPosition;
import relocate.org.objectweb.asm.ClassReader;
import relocate.org.objectweb.asm.Type;
import relocate.org.objectweb.asm.tree.*;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Modifier;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 使用asm技术实现
 * 搜索所有类中的cb和nms调用,并判断在其后的版本中,是否有该api
 * 把没有的打印出来
 * <p>
 * 会忽略@CompatibilityChecked注解的类,也可设置强制扫描
 *
 * @author pyz
 * @date 2019/7/14 7:48 PM
 */
public class CompatibilityChecker {
    private static final List<String> versions = new ArrayList<>();
    private static final Pattern cbPattern = Pattern.compile("org/bukkit/craftbukkit/(v\\d_\\d+_R\\d)/[\\w/]+");
    private static final Pattern nmsPattern = Pattern.compile("net/minecraft/server/(v\\d_\\d+_R\\d)/[\\w/]+");
    private static Set<String> ignores = new HashSet<>();
    private ClassNode curClass;
    private MethodNode curMethod;
    //    是否无视@CompatibilityChecked注解
    private boolean forceCheck = false;

    static {
        versions.add("v1_7_R4");
        versions.add("v1_8_R3");
        versions.add("v1_9_R2");
        versions.add("v1_10_R1");
        versions.add("v1_11_R1");
        versions.add("v1_12_R1");
        versions.add("v1_13_R2");
        versions.add("v1_14_R1");
        versions.add("v1_15_R1");
        versions.add("v1_16_R3");
//        versions.add("v1_17_R1");
    }

    @org.junit.Test
    public void check() throws IOException, ClassNotFoundException {
        Class blockPortalClass = BlockPosition.class;
        blockPortalClass.getDeclaredMethods();
        String userDir = System.getProperty("user.dir");
        List<String> paths = new ArrayList<>();
        Files.walkFileTree(Paths.get(userDir), new FileVisitor<Path>() {
            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                if (Files.isRegularFile(file) && file.toFile().getName().endsWith(".java")) {
                    String str = file.toFile().getAbsolutePath().substring(userDir.length() + 1);
                    if (!str.startsWith("sbp_")) {
                        return FileVisitResult.CONTINUE;
                    }
                    str = str.replaceAll("^.*src/main/java/", "");
                    str = str.substring(0, str.length() - 5);
                    str = str.replace('/', '.');
                    if (str.contains(".test.")) {
                        return FileVisitResult.CONTINUE;
                    }
                    paths.add(str);
                }
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                return FileVisitResult.CONTINUE;
            }
        });
        for (String path : paths) {
            resolve(path);
        }
    }

    public void resolve(String className) throws IOException {
        if (className.contains("package-info")) {
            return;
        }
        curClass = getClassNode(className, false);
        if (curClass == null) {
            System.out.println("can not find class: " + className);
        }
        if (curClass.visibleAnnotations != null) {
            for (AnnotationNode visibleAnnotation : curClass.visibleAnnotations) {
                if (Type.getType(visibleAnnotation.desc).getClassName().equals("lol.clann.minecraft.springboot.api.annotation.compatibility.CompatibilityChecked")) {
                    System.out.println("class has been checked. Ignore it:" + className);
                    return;
                }
            }
        }
        for (MethodNode method : curClass.methods) {
            curMethod = method;
            resolve(method);
        }
    }

    private void resolve(MethodNode methodNode) throws IOException {
        ListIterator<AbstractInsnNode> it = methodNode.instructions.iterator();
        while (it.hasNext()) {
            AbstractInsnNode insn = it.next();
            if (insn instanceof MethodInsnNode) {
                MethodInsnNode methodInsnNode = (MethodInsnNode) insn;
                String key = methodInsnNode.owner + " " + methodInsnNode.name + " " + methodInsnNode.desc;
                if (!ignores.add(key)) {
                    continue;
                }
                for (int i = 0; i < versions.size(); i++) {
                    resolve(methodInsnNode, i);
                }
            } else if (insn instanceof FieldInsnNode) {
                FieldInsnNode fieldInsnNode = (FieldInsnNode) insn;
                String key = fieldInsnNode.owner + " " + fieldInsnNode.name + " " + fieldInsnNode.desc;
                if (!ignores.add(key)) {
                    continue;
                }
                for (int i = 0; i < versions.size(); i++) {
                    resolve(fieldInsnNode, i);
                }
            }
        }
    }

    private void resolve(FieldInsnNode fieldInsnNode, int versionIndex) throws IOException {
        MethodInfo mi = new MethodInfo(fieldInsnNode.owner, fieldInsnNode.name, fieldInsnNode.desc);
        if (mi.srcVersion == null) {
            return;
        }
        if (mi.versionIndex > versionIndex) {
            return;
        }
        String version = versions.get(versionIndex);
        Result result = mi.resolveField(version);
        if (result.getStatus() == 0) {
            return;
        }
        System.out.println(String.format("%s %s %s %s %s %s %s %s %s %s %s",
                curClass.name.substring(curClass.name.lastIndexOf('/') + 1),
                curMethod.name,
                result.getStatus() == 0 ? "OK" : "ERROR",
                mi.owner,
                mi.name,
                mi.desc,
                version,
                result.getStatus() != 1,
                result.getStatus() != 2,
                result.getStatus() != 3,
                result.getErrorMsg()
        ));
    }

    private void resolve(MethodInsnNode methodInsnNode, int versionIndex) throws IOException {
        CompatibilityChecker.MethodInfo mi = new CompatibilityChecker.MethodInfo(methodInsnNode.owner, methodInsnNode.name, methodInsnNode.desc);
        if (mi.srcVersion == null) {
            return;
        }
        if (mi.versionIndex >= versionIndex) {
            return;
        }
        String version = versions.get(versionIndex);
        Result result = mi.resolveMethod(version);
        if (result.getStatus() == 0) {
            return;
        }
        System.out.println(String.format("%s %s %s %s %s %s %s %s %s %s %s",
                curClass.name.substring(curClass.name.lastIndexOf('/') + 1),
                curMethod.name,
                result.getStatus() == 0 ? "OK" : "ERROR",
                mi.owner,
                mi.name,
                mi.desc,
                version,
                result.getStatus() != 1,
                result.getStatus() != 2,
                result.getStatus() != 3,
                result.getErrorMsg()
        ));
    }

    public static ClassNode getClassNode(String classnName, boolean print) throws IOException {
        InputStream in = CompatibilityChecker.class.getClassLoader().getResourceAsStream(classnName.replace('.', '/') + ".class");
        if (in == null) {
            return null;
        }
        byte[] code = IOUtils.readBytes(in);
        if (print) {
            ASMUtils.printClass(code);
        }
        ClassReader reader = new ClassReader(code);
        ClassNode node = new ClassNode();
        reader.accept(node, 0);
        return node;
    }

    public static class MethodInfo {
        public int versionIndex = -1;
        public String srcVersion;
        public String owner;
        public String name;
        public String desc;

        public MethodInfo(String owner, String name, String desc) {
            this.owner = owner;
            this.name = name;
            this.desc = desc;
            Matcher m = cbPattern.matcher(owner);
            srcVersion = null;
            if (m.find()) {
                srcVersion = m.group(1);
            } else {
                m = nmsPattern.matcher(owner);
                if (m.find()) {
                    srcVersion = m.group(1);
                }
            }
            if (srcVersion != null) {
                versionIndex = versions.indexOf(srcVersion);
            }
        }

        public Result resolveMethod(String version) throws IOException {
            String owner = this.owner.replace(srcVersion, version);
            ClassNode node = getClassNode(owner, false);
            if (node == null) {
                return Result.fail(1).setErrorMsg("method owner not found");
            }
            return resolveMethod(node, version);
        }

        public Result resolveMethod(ClassNode node, String version) throws IOException {
            String name = this.name;
            String desc = this.desc.replace(srcVersion, version);
            Type[] args1 = Type.getArgumentTypes(desc);
            Type returmType = Type.getReturnType(desc);
            if (node == null) {
                return Result.fail(1).setErrorMsg("method owner not found");
            }
            for (MethodNode method : node.methods) {
                if (method.name.equals(name)) {
                    Type[] args2 = Type.getArgumentTypes(method.desc);
                    if (Arrays.equals(args1, args2)) {
                        if (!returmType.equals(Type.getReturnType(method.desc))) {
                            return Result.fail(3).setErrorMsg("method desc not match " + returmType.getInternalName() + " -> " + Type.getReturnType(method.desc).getInternalName());
                        } else {
                            if (Modifier.isPublic(method.access)) {
                                return Result.fail(0);
                            } else {
                                return Result.fail(2).setErrorMsg("method is not public");
                            }

                        }
                    }
                }
            }
            List<String> parentClasses = new ArrayList<>();
            if (node.superName != null) {
                parentClasses.add(node.superName.replace('/', '.'));
            }
            if (node.interfaces != null) {
                for (String anInterface : node.interfaces) {
                    parentClasses.add(anInterface.replace('/', '.'));
                }
            }
            Result r;
            for (String parentClass : parentClasses) {
                r = resolveMethod(getClassNode(parentClass, false), version);
                if (r.getStatus() != 2) {
                    return r;
                }
            }
            return Result.fail(2).setErrorMsg("no same name method");
        }

        public Result resolveField(String version) throws IOException {
            String owner = this.owner.replace(srcVersion, version);
            ClassNode node = getClassNode(owner, false);
            if (node == null) {
                return Result.fail(1).setErrorMsg("field owner not found");
            }
            return resolveField(node, version);
        }

        public Result resolveField(ClassNode node, String version) throws IOException {
            String name = this.name;
            String desc = this.desc.replace(srcVersion, version);
            if (node == null) {
                return Result.fail(1);
            }
            for (FieldNode field : node.fields) {
                if (field.name.equals(name)) {
                    if (field.desc.equals(desc)) {
                        return Result.fail(0);
                    } else {
                        return Result.fail(3).setErrorMsg("field desc not match " + desc + " -> " + field.desc);
                    }
                }
            }
            if (node.superName != null) {
                return resolveField(getClassNode(node.superName.replace('/', '.'), false), version);
            }
            return Result.fail(2).setErrorMsg("not found same name Field");
        }
    }
}
